﻿/*
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.

Copyright (C) 2005-2014  Sylvain Rougeaux (Cyber Sinh) (http://www.luminescence-software.org/)
*/

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace Luminescence.Xiph
{
   /// <summary>
   ///   Classe de lecture / écriture des tags des fichiers FLAC natifs.
   /// </summary>
   public sealed class FlacTagger : VorbisComment
   {
      // Identificateurs
      private static readonly byte[] fLaC = { 0x66, 0x4C, 0x61, 0x43 };

      // Informations utiles pour la réécriture du fichier
      private long offsetAudioFrameStart;

      private int desiredPaddingSize = 4096;

      ///<summary>
      ///  Taille souhaitée du bloc PADDING lorsque la reconstruction du fichier FLAC s'avère nécessaire.
      ///</summary>
      public int DesiredPaddingSize
      {
         get { return desiredPaddingSize; }
         set { desiredPaddingSize = value < 0 ? 0 : value; }
      }

      private bool optimizeFileSizeWhenSaving;

      ///<summary>
      ///  Indique s'il convient de recupérer l'espace disque laissé vacant suite à la suppression de tags.
      ///</summary>
      public bool OptimizeFileSizeWhenSaving
      {
         get { return optimizeFileSizeWhenSaving; }
         set
         {
            optimizeFileSizeWhenSaving = value;
            if (value)
               desiredPaddingSize = 0;
         }
      }

      // APPLICATION, SEEKTABLE, CUESHEET
      private readonly List<byte[]> metadataBlocks = new List<byte[]>(3);

      /// <summary>
      ///   Constructeur de la classe FlacTagger.
      /// </summary>
      /// <param name = "fileName">Chemin d'accès du fichier FLAC à analyser</param>
      public FlacTagger(string fileName)
      {
         ReadMetadata(fileName);
      }

      /// <summary>
      ///   Lit les métadonnées du fichier FLAC.
      /// </summary>
      /// <param name = "fileName">Chemin d'accès du fichier FLAC à analyser</param>
      public override void ReadMetadata(string fileName)
      {
         FileName = fileName;
         ClearTags();
         ClearArts();
         metadataBlocks.Clear();

         using (var fs = new FileStream(FileName, FileMode.Open, FileAccess.Read, FileShare.Read))
         {
            Length = fs.Length;

            if (Length == 0)
               throw new FileFormatException("The file is not a valid FLAC file because it is empty.");

            var br = new BinaryReader(fs);
            if (!fLaC.SequenceEqual(br.ReadBytes(4)))
               throw new FileFormatException("The file is not a valid FLAC file.");

            fs.Position = 18;
            byte[] buffer = br.ReadBytes(8);

            SampleRate = buffer[0];
            SampleRate = (SampleRate << 8) | buffer[1];
            SampleRate = (SampleRate << 4) | (buffer[2] >> 4);

            Channels = (byte)(((buffer[2] >> 1) & 7) + 1);

            BitsPerSample = (byte)((((buffer[2] & 1) << 4) | (buffer[3] >> 4)) + 1);

            Samples = (buffer[3] & 15);
            Samples = (Samples << 8) | buffer[4];
            Samples = (Samples << 8) | buffer[5];
            Samples = (Samples << 8) | buffer[6];
            Samples = (Samples << 8) | buffer[7];

            fs.Position = 42; // first metadata block following STREAM_INFO
            bool last_metadata_block;
            do
            {
               byte startHeader = br.ReadByte();
               last_metadata_block = (startHeader >> 7) == 1;
               var blockType = (FlacMetadataBlock)(startHeader & 127);

               buffer = br.ReadBytes(3);
               int size = buffer[0];
               size = (size << 8) | buffer[1];
               size = (size << 8) | buffer[2];

               switch (blockType)
               {
                  case FlacMetadataBlock.StreamInfo:
                     throw new FileFormatException("The file contains several Stream Info blocks.");
                  case FlacMetadataBlock.Padding:
                     fs.Position += size;
                     break;
                  case FlacMetadataBlock.VorbisComment:
                     ReadVorbisComment(fs, false);
                     break;
                  case FlacMetadataBlock.Picture:
                     buffer = br.ReadBytes(size);
                     AddArt(new ID3PictureFrame(buffer));
                     break;
                  default: // APPLICATION, SEEKTABLE, CUESHEET, other blocks
                     var metadata = new byte[size + 4];
                     metadata[0] = (byte)blockType;
                     buffer.CopyTo(metadata, 1);
                     buffer = br.ReadBytes(size);
                     buffer.CopyTo(metadata, 4);
                     metadataBlocks.Add(metadata);
                     break;
               }
            } while (!last_metadata_block);

            if (Vendor == null)
               throw new FileFormatException("The Vorbis comment header cannot be found in the file.");

            offsetAudioFrameStart = fs.Position;
         }
      }

      /// <summary>
      ///   Enregistre les tags dans le fichier FLAC.
      /// </summary>
      public override void SaveMetadata()
      {
         byte[] buffer = PrepareFlacHeader();
         int paddingSize = (int)offsetAudioFrameStart - buffer.Length - 42 - 4;

         if (paddingSize < 0 || optimizeFileSizeWhenSaving)
            SaveMetadata(buffer, desiredPaddingSize);
         else
            SaveMetadataInSitu(buffer, paddingSize);
      }

      private byte[] PrepareFlacHeader()
      {
         using (var ms = new MemoryStream())
         {
            foreach (byte[] metadataBlock in metadataBlocks)
               ms.Write(metadataBlock, 0, metadataBlock.Length);

            byte[] buffer;

            foreach (ID3PictureFrame picture in Arts)
            {
               buffer = CreateBlock(picture.ToApicFrame(), FlacMetadataBlock.Picture, false);
               ms.Write(buffer, 0, buffer.Length);
            }

            buffer = CreateBlock(WriteVorbisComment(false, false), FlacMetadataBlock.VorbisComment, false);
            ms.Write(buffer, 0, buffer.Length);

            return ms.ToArray(); 
         }
      }

      private void SaveMetadataInSitu(byte[] flacHeader, int paddingSize)
      {
         using (var fsSource = new FileStream(FileName, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
         {
            fsSource.Position = 42;
            fsSource.Write(flacHeader, 0, flacHeader.Length);

            byte[] buffer = CreateBlock(new byte[paddingSize], FlacMetadataBlock.Padding, true);
            fsSource.Write(buffer, 0, buffer.Length);
         }
      }

      internal static byte[] CreateBlock(byte[] data, FlacMetadataBlock block, bool lastBlock)
      {
         var buffer = new byte[data.Length + 4];
         buffer[0] = lastBlock ? (byte)((byte)block | 128) : (byte)block;
         buffer[1] = (byte)(data.Length >> 16);
         buffer[2] = (byte)(data.Length >> 8);
         buffer[3] = (byte)(data.Length);

         data.CopyTo(buffer, 4);

         return buffer;
      }

      private void SaveMetadata(byte[] flacHeader, int paddingSize)
      {
         string tempFilename = FileName + ".tmp";
         FileStream fsSource = null;
         FileStream fsDest = null;
         try
         {
            fsSource = new FileStream(FileName, FileMode.Open, FileAccess.Read, FileShare.Read);
            fsDest = new FileStream(tempFilename, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
            var br = new BinaryReader(fsSource);

            byte[] buffer = br.ReadBytes(42); // data from 0 to first metadata block following STREAM_INFO
            fsDest.Write(buffer, 0, buffer.Length);

            fsDest.Write(flacHeader, 0, flacHeader.Length);

            buffer = CreateBlock(new byte[paddingSize], FlacMetadataBlock.Padding, true);
            fsDest.Write(buffer, 0, buffer.Length);

            fsSource.Position = offsetAudioFrameStart;
            fsSource.CopyTo(fsDest); // copy last part from old file to new one

            fsSource.Dispose();
            fsDest.Dispose();
            File.SetAttributes(FileName, FileAttributes.Normal);
            File.Replace(tempFilename, FileName, null, true);
         }
         catch
         {
            if (fsSource != null) fsSource.Dispose();
            if (fsDest != null)
            {
               fsDest.Dispose();
               if (File.Exists(tempFilename)) 
                  File.Delete(tempFilename);
            }

            throw;
         }
      }

      #region Informations techniques

      /// <summary>
      ///   Retourne le chemin et le nom du fichier FLAC analysé.
      /// </summary>
      public string FileName { get; private set; }

      /// <summary>
      ///   Retourne le nombre de canaux dans le fichier FLAC.
      /// </summary>
      public byte Channels { get; private set; }

      /// <summary>
      ///   Retourne le taux d'échantillonage du fichier FLAC.
      /// </summary>
      public int SampleRate { get; private set; }

      /// <summary>
      ///   Retourne la version du codec utilisé.
      /// </summary>
      public string CodecVersion
      {
         get
         {
            string[] buffer = Vendor.Split(new[] { ' ' });
            return buffer.Length > 2 ? buffer[2] : Vendor;
         }
      }

      /// <summary>
      ///   Retourne la taille du fichiers FLAC (en octets).
      /// </summary>
      public long Length { get; private set; }

      /// <summary>
      ///   Retourne le nombre d'échantillon contenu dans le fichier FLAC.
      /// </summary>
      public long Samples { get; private set; }

      /// <summary>
      ///   Retourne la durée du fichier FLAC (en secondes).
      /// </summary>
      public float Duration => (float)Math.Round((double)Samples / SampleRate, 3);

      /// <summary>
      ///   Retourne le bitrate moyen du fichier FLAC.
      /// </summary>
      public int AverageBitrate => (int)Math.Round((double)Length * 8 / Duration, 0);

      /// <summary>
      ///   Retourne le taux de compression du ficher FLAC.
      /// </summary>
      public float Ratio => (float)Length / UncompressedFileLength;

      /// <summary>
      ///   Retourne la taille de l'échantillon du fichier FLAC.
      /// </summary>
      public byte BitsPerSample { get; private set; }

      /// <summary>
      ///   Retourne la taille du fichier décompressée (en octets).
      /// </summary>
      public long UncompressedFileLength => (long)Math.Round(((double)(Duration * SampleRate * BitsPerSample * Channels / 8) + 44), 0);

      #endregion
   }
}